package com.xyq.gateway.filter;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.xyq.common.core.constants.CacheConstants;
import com.xyq.common.core.constants.Constants;
import com.xyq.common.core.constants.HttpConstants;
import com.xyq.common.core.domain.Result;
import com.xyq.common.core.enums.ResultCode;
import com.xyq.common.core.enums.UserIdentity;
import com.xyq.common.core.domain.LoginUser;
import com.xyq.common.core.utils.JwtUtils;
import com.xyq.common.core.utils.ThreadLocalUtil;
import com.xyq.common.redis.service.RedisService;
import com.xyq.gateway.proterties.IgnoreWhiteProperties;

import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;

/**
 * ⽹关鉴权
 *
 */
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {
    // 排除过滤的 uri ⽩名单地址，在nacos⾃⾏添加
    @Autowired
    private IgnoreWhiteProperties ignoreWhite;
    @Value("${jwt.secret}")
    private String secret;
    @Autowired
    private RedisService redisService;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String url = request.getURI().getPath();    // 1. 获取请求中的路径
        // 跳过不需要验证的路径
        if (matches(url, ignoreWhite.getWhites())) {  // 2. 判断 这个路径 是否在 白名单里,如果在就跳过
            return chain.filter(exchange);            // 例如 : login这样的登录请求,就不需要进行处理
        }

        //从http请求头中获取token
        String token = getToken(request);       //3. 从请求中获取 token
        if (StrUtil.isEmpty(token)) {          // 如果token是空就直接返回
            return unauthorizedResponse(exchange, "令牌不能为空");
        }
        Claims claims;
        try {                                 //4.从把token 反序列化,解析回 Key,value的形式 ,拿回token中的信息
            claims = JwtUtils.parseToken(token, secret); //获取令牌中信息 解析payload中信息
            if (claims == null) {
                return unauthorizedResponse(exchange, "令牌已过期或验证不正确！");
            }
        } catch (Exception e) {
            return unauthorizedResponse(exchange, "令牌已过期或验证不正确！");
        }
                  // 5.  我们创建token时,token里的信息有 一个 userId 和 userKey 唯一标识
                 //   同时 在 redis里存储的信息时: "tokenKey:唯一身份标识"
        String userKey = JwtUtils.getUserKey(claims); //获取jwt中的key ,jwt里存的时 "userKey:唯一身份标识" 通过hash把value:唯一身份标识拿出来
        boolean isLogin = redisService.hasKey(getTokenKey(userKey)); // getTokenKsy:是因为redis里存放的key是: "tokenKey:身份标识" , 所以要
        if (!isLogin) {                                                  // 先把key 给 得到 就是 "tokenKey" + userKey
            return unauthorizedResponse(exchange, "登录状态已过期");      // 然后判断 redis里有没有这个key , 没有就是已经过期或失效
        }
        String userid = JwtUtils.getUserId(claims); //判断jwt中的信息是否完整  6. 拿到userId,判断token里的userId是否为空
        if (StrUtil.isEmpty(userid)) {
            return unauthorizedResponse(exchange, "令牌验证失败");
        }                    //  7. redis里存储的key是上面得到的userKey , value是 LoginUser 对象
                             // 通过key 获取 Value , 看value是否正确
        LoginUser user = redisService.getCacheObject(getTokenKey(userKey), LoginUser.class);
                            // 8. LoginUser 的identity 分两种 一个是1普通用户 , 一个是 2管理员用户 : 两个if判断普通用户和管理员用户
            // 这个 url.constais 里是用来判断 url里是否带有 /system/ 的字段
            // 因为前面 对路由设置了,要进入管理员系统,url里必须带有/system/的字段, 网关才会把请求转发到 管理员的微服务里
               // 是管理员,所以 identity 的值应该是 2 , 也就是这个UserIdentity枚举里的ADMIN
        if (url.contains(HttpConstants.SYSTEM_URL_PREFIX) && !UserIdentity.ADMIN.getValue().equals(user.getIdentity())) {
            return unauthorizedResponse(exchange, "令牌验证失败");
        }
               // 9. 和管理员相反 , 这里面的普通用户里的 url里应该带有friend ,          同时 identity应该是 ORDINARY
        if (url.contains(HttpConstants.FRIEND_URL_PREFIX) && !UserIdentity.ORDINARY.getValue().equals(user.getIdentity())) {
            return unauthorizedResponse(exchange, "令牌验证失败");
        }
              // 10.如果 token存在, 同时token在redis里也存在 , 同时 redis里的value也对应 那么才是正确的

        //  当所有都验证完成后 , 把userid 添加到ThreadLocal

        return chain.filter(exchange);
    }

    /**
     * 查找指定url是否匹配指定匹配规则链表中的任意⼀个字符串
     *
     * @param url 指定url
     * @param patternList 需要检查的匹配规则链表
     * @return 是否匹配
     */                 // 比较url 是否在白名单里配置有 ,如果白名单里配置有就返回true
    private boolean matches(String url, List<String> patternList) {
        if (StrUtil.isEmpty(url) || CollectionUtils.isEmpty(patternList)) {
            return false;
        }
        for (String pattern : patternList) {
            if (isMatch(pattern, url)) {
                return true;
            }
        }
        return false;
    }
    /**
     * 判断url是否与规则匹配
     * 匹配规则中：
     * ? 表⽰单个字符;
     * * 表⽰⼀层路径内的任意字符串，不可跨层级;
     * ** 表⽰任意层路径;
     *
     * @param pattern 匹配规则
     * @param url 需要匹配的url
     * @return 是否匹配
     */
    private boolean isMatch(String pattern, String url) {
        AntPathMatcher matcher = new AntPathMatcher();
        return matcher.match(pattern, url);
    }
    /**
     * 获取缓存key
     */
    private String getTokenKey(String token) {
        return CacheConstants.LOGIN_TOKEN_KEY + token;
    }
    /**
     * 从请求头中获取请求token
     */                                    // 1. header中Authorization字段 里就是专门存储token的的地方
    private String getToken(ServerHttpRequest request) {
        String token = request.getHeaders().getFirst(HttpConstants.AUTHENTICATION);
        // 如果前端设置了令牌前缀，则裁剪掉前缀         // Bearer请求头中 token可能会带有的前缀, 把这个Bearer前缀去除掉
        if (StrUtil.isNotEmpty(token) && token.startsWith(HttpConstants.PREFIX)) {
            token = token.replaceFirst(HttpConstants.PREFIX, StrUtil.EMPTY);
        }
        return token;
    }
             // 当验证不通过, 返回的其实是 Result (3001未授权验证码, msg) msg是上面方法里的那些
    private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) {
        log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
        return webFluxResponseWriter(exchange.getResponse(), msg, ResultCode.FAILED_UNAUTHORIZED.getCode());
    }
    //拼装webflux模型响应      // 没有使用throws一个异常,是因为我们配置的 统一异常处理,无法处理这里的异常
                           //  spring cloud gateway 是基于webflux的所以 他是在更前面一点,
                          //统一异常处理是在springmvc上的 , 而gateway是在webflux
    private Mono<Void> webFluxResponseWriter(ServerHttpResponse response, String msg, int code) {
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        Result<?> result = Result.fail(code, msg);    // 同时这里返回的数据也应该是 Result类型的 : 需要给 Result添加fail方法
        DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(result).getBytes());
        return response.writeWith(Mono.just(dataBuffer));
    }
    @Override
    public int getOrder() {
        return -200;   // 值越小 , 过滤器就越先被执行
    }

}